/*
 * This file or a portion of this file is licensed under the terms of
 * the Globus Toolkit Public License, found in file ../GTPL, or at
 * http://www.globus.org/toolkit/download/license.html. This notice must
 * appear in redistributions of this file, with or without modification.
 *
 * Redistributions of this Software, with or without modification, must
 * reproduce the GTPL in: (1) the Software, or (2) the Documentation or
 * some other similar material which is provided with the Software (if
 * any).
 *
 * Copyright 1999-2004 University of Chicago and The University of
 * Southern California. All rights reserved.
 */
package org.griphyn.vdl.router;
import org.griphyn.vdl.classes.*;
import org.griphyn.vdl.util.*;
import org.griphyn.vdl.dax.*;

// java.util.List clashes with org.griphyn.vdl.classes.List
import java.util.HashMap;
import java.util.Vector;
import java.util.HashSet;
import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;
import java.io.*;

/**
 * This class stores state when constructing the DAG for output. It
 * is expected that for each DAG generation, one instance of this
 * class performs the state tracking. The class is tightly coupled
 * to the class that performs the routing.
 *
 * @author Jens-S. Vöckler
 * @author Yong Zhao
 * @version $Revision: 50 $
 * @see Route
 */
public class BookKeeper 
{
  /**
   * This contains the set of visited derivations.
   */
  private HashMap m_visited;

  /**
   * This map tracks the set of parents for each child derivation.
   * Each value item in the map is a set.
   */
  private HashMap m_parent;

  /**
   * The content of the abstract DAG for XML (DAX) is created after
   * the alternatives are found. Maybe in a later version, it will
   * be created in parallel with adding derivations?
   */
  private ADAG m_dax;

  /**
   * Unfortunately, for this release, we need to map the shortID of
   * a derivation to some number that is acceptable to NMTOKEN, ID and
   * IDREF.
   */
  private SequenceMapping m_mapping;

  /**
   * The <code>Profile</code> elements in nested compound TR can 
   * be equally nested.
   */
  private ListStack m_profileStack;

  /**
   * The names of compound transformations that we got through. This
   * data will also be added to the job description once a job is added
   * to the book-keeper.
   */
  private ArrayList m_transformations;

  /**
   * The temporary files must be unique on a per-workflow basis.
   */
  private HashSet m_tempfiles;

  /**
   * To create temporary filenames, we need an entropy source. 
   */
  private Random m_rng;

  /**
   * Default ctor.
   */
  public BookKeeper()
  {
    this.m_parent = new HashMap();
    this.m_visited = new HashMap();
    this.m_dax = new ADAG();
    this.m_mapping = new SequenceMapping();
    this.m_profileStack = new ListStack();
    this.m_transformations = new ArrayList();
    this.m_tempfiles = new HashSet();
    this.m_rng = new Random();
  }

  /**
   * Accessor: push a vector of profiles down the profile stack.
   * @param profiles is a list of profiles, may be empty.
   * @see #popProfile()
   */
  public void pushProfile( java.util.List profiles )
  {
    this.m_profileStack.push(profiles);
  }

  /**
   * Accessor: pop a vector of profiles from the profile stack.
   * @return the last active vector of profiles, may be empty.
   * @see #pushProfile( java.util.List )
   */
  public java.util.List popProfile()
  {
    return this.m_profileStack.pop();
  }

  /**
   * Accessor: obtains all profiles on the profile stack.
   * @return all stacked profiles in one structure, may be empty.
   */
  public java.util.List getAllProfiles()
  {
    return this.m_profileStack.flatten();
  }

  /**
   * Accessor: push the FQDI of a transformation onto a stack.
   * @param fqdi is the fully-qualified definition identifier.
   * @see #popTransformation()
   */
  public void pushTransformation( String fqdi )
  {
    this.m_transformations.add(fqdi);
  }

  /**
   * Accessor: pop an FQDI from the stack of transformations.
   * @return the last FQDI pushed, may be empty for an empty stack.
   * @see #pushTransformation( String )
   */
  public String popTransformation()
  {
    int pos = this.m_transformations.size()-1;
    if ( pos >= 0 ) return (String) this.m_transformations.remove(pos);
    else return null;
  }

  /**
   * Accessor: obtains all transformations that were pushed as space
   * separated string. 
   *
   * @return all transformation names, may be empty.
   * @see #addJob( Job )
   */
  public String getAllTransformations()
  {
    StringBuffer result = new StringBuffer();

    for ( Iterator i=this.m_transformations.iterator(); i.hasNext(); ) {
      result.append( (String) i.next() );
      if ( i.hasNext() ) result.append(' ');
    }

    return result.toString();
  }

  /**
   * Accessor: Returns the constructed internal DAX keeper.
   *
   * @return the abstract DAX description. Note that this may be empty!
   */
  public ADAG getDAX( String name )
  {
    this.m_dax.setName(name);
    return this.m_dax;
  }

  /**
   * Accessor: Appends a job definition to the DAX structure.
   */
  public boolean addJob( Job job )
  {
    return this.m_dax.addJob(job);
  }

  /**
   * This method updates the "cursor" position with a new derivation.
   * The cursor helps tracking derivations while a DAG is being produced.
   *
   * @param dv is the new current derivation that the cursor will be set to.
   * @see #getCurrent()
   */
  public void setCurrent( HasPass dv )
  { 
    // assign a new cursor
    Logging.instance().log( "state", 2,
			    "NOOP: setting cursor to " + dv.identify() );
  }

  /**
   * Obtains the current cursor position. 
   * @return the derivation that the cursor is located at.
   * @see #setCurrent( HasPass )
   */
  public HasPass getCurrent()
  { 
    throw new RuntimeException( "method not implemented" );
  }

  /**
   * Maps a DV identification to a name that can be put into the XML
   * datatypes NMTOKEN, ID and IDREF. The identification used to be
   * the DV's short ID, but recent changes use the full ID.  
   *
   * @param id is the derivation identifier
   * @return an XML-compatible job id
   */
  public String mapJob( String id )
  {
    String s = this.m_mapping.forward(id);
    Logging.instance().log( "state", 3, "mapJob(" + id + ") = " + s );
    return s;
  }

  /**
   * Obtains an existing mapping of a DV indentification to a job id.
   * The identification used to be the DV's short ID, but recent changes
   * use the full ID.
   *
   * @param id is the derivation identifier
   * @return a job identifier, or null if no such mapping exists.
   */
  public String jobOf( String id )
  {
    String s = this.m_mapping.get(id);
    Logging.instance().log( "state", 3, "jobOf(" + id + ") = " + 
			    (s == null ? "(null)" : s) );
    return s;
  }

  /** 
   * Accessor: Obtains the level of a given job from the DAX structure.
   * @param jobid is the job identifier
   * @return the level of the job, or -1 in case of error (no such job). 
   */
  private int getJobLevel( String jobid )
  {
    Job j = this.m_dax.getJob(jobid);
    return ( j == null ? -1 : j.getLevel() );
  }

  /**
   * Accessor: Recursively adjusts job level backwards.
   *
   * @see org.griphyn.vdl.dax.ADAG#adjustLevels( String, int )
   */
  private int adjustJobLevels( String startjob, int distance )
  {
    return this.m_dax.adjustLevels( startjob, distance );
  }

  /**
   * Adds a parent set for the given element to the state
   * @param current is the current derivation to add parents for
   * @param parents are the parents, any number, to add for the given element
   * @return true, if the parent was not know before or the currelem is empty,
   * false, if the parent was added previously.
   */
  public boolean addParent( HasPass current, Set parents )
  {
    String id = current.identify();

    if ( parents.size() == 0 ) {
      // nothing to do
      Logging.instance().log( "state", 4, "no parents for " + id );
      return false;
    }

    if ( ! this.m_parent.containsKey(id) ) {
      Logging.instance().log( "state", 4, "adding new parent space for " + id );
      this.m_parent.put( id, new TreeSet() );
    }

    // check for modifications
    boolean modified = ((Set) this.m_parent.get(id)).addAll( parents );
    if ( modified ) {
      Logging.instance().log( "state", 4, "CHILD " + id + " PARENT " +
			      this.m_parent.get(id).toString() );

      // add to parent/child DAX relationship			// OLD
      // NEW: adjust job level count as necessary
      String jobid = mapJob(id);				// OLD
      int level = getJobLevel(jobid);
      for ( Iterator i=parents.iterator(); i.hasNext(); ) {	// OLD
	String pid = mapJob((String) i.next());			// OLD
	this.m_dax.addChild( jobid, pid );			// OLD
	int plevel = getJobLevel(pid);
	if ( level != -1 && plevel != -1 && level >= plevel ) {
	  int adjustment = level - plevel + 1;
 	  Logging.instance().log( "state", 3, "adjusting levels starting at " +
 				  pid + " by " + adjustment );

	  adjustJobLevels( pid, adjustment );
	}
      }
    }

    return modified;
  }

  /**
   * This method marks a derivation as visited in the set of visited nodes.
   *
   * @param current is the caller to add to the visited node set.
   * @param real is the value to add for the given caller.
   * @return <code>true</code>, if the caller was previously unknown
   * to the visited set.
   */
  public boolean addVisited( HasPass current, Set real )
  { 
    String id = current.identify();
    boolean exists = this.m_visited.containsKey(id);
    if ( exists ) {
      Set temp = (Set) this.m_visited.get(id);
      Logging.instance().log( "state", 2, "already visited node " + id + 
			      " keeping " + temp );
    } else {
      this.m_visited.put( id, real );
      Logging.instance().log( "state", 2, "adding visited node " + id + 
			      " with " + real );
    }
    return exists;
  }

  /**
   * Checks if a node was previously visited. The visited nodes have to
   * be tracked in any kind of breadth first and depth first search.
   *
   * @param dv is the derivation to check, if it was visited before.
   * @return true, if the derivation was already visited previously.
   */
  public boolean wasVisited( HasPass dv )
  {
    String id = dv.identify();
    boolean result = this.m_visited.containsKey(id); 
    Logging.instance().log( "state", 5, "wasVisited(" + id + ") = " + result );
    return result;
  }

  public Set getVisited( HasPass dv )
  {
    String id = dv.identify();
    Set result = (Set) this.m_visited.get(id);
    if ( result == null ) result = new TreeSet();
    Logging.instance().log( "state", 5, "getVisited(" + id + ") = " + result );
    return result;
  }

  /**
   * This method helps to track the input and output files. From this
   * information, the input, intermediary and output files of the
   * complete DAG can be constructed. This method does allow for inout.
   *
   * @param lfnset is a set of LFN instances, which encapsulate their
   * respective linkage.
   * @see java.util.AbstractCollection#addAll( java.util.Collection )
   */
  public void addFilenames( Collection lfnset )
  {
    if ( lfnset == null )
      return; // nothing to do

    for ( Iterator i=lfnset.iterator(); i.hasNext(); ) {
      LFN lfn = (LFN) i.next();
      String name = lfn.getFilename();
      if ( lfn.getLink() == LFN.INPUT || lfn.getLink() == LFN.INOUT ) {
	this.m_dax.addFilename( name, true , lfn.getTemporary(), 
				lfn.getDontRegister(), lfn.getDontTransfer() );
	Logging.instance().log( "state", 5, "adding input filename " + name );
      }
      if ( lfn.getLink() == LFN.OUTPUT || lfn.getLink() == LFN.INOUT ) {
	this.m_dax.addFilename( name, false, lfn.getTemporary(), 
				lfn.getDontRegister(), lfn.getDontTransfer() );
	Logging.instance().log( "state", 5, "adding output filename " + name );
      }
    }
  }

  /**
   * String constant describing the six X.
   */
  static final String XXXXXX = "XXXXXX";

  /**
   * Where to draw filename characters from.
   */
  private static final String filenamechars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";

  /**
   * Creates a unique temporary filename. The new name is registered
   * locally to ensure uniqueness. A string of multiple capitol X, at
   * least six, is replaced with some random factor. 
   *
   * @param hint is a filename hint.
   * @param suffix is the suffix for the filename. 
   * @return a somewhat unique filename - for this workflow only.
   */
  public String createTempName( String hint, String suffix )
  {
    String result = null;

    // sanity checks
    if ( hint == null || hint.length() == 0 ) hint = XXXXXX;
    if ( suffix == null ) suffix = ".tmp";

    // where are the X
    int where = hint.lastIndexOf( XXXXXX );
    if ( where == -1 ) {
      hint += XXXXXX;
      where = hint.lastIndexOf( XXXXXX );
    }

    // create a changable buffer
    StringBuffer sb = new StringBuffer( hint );

    // find end of substitution area
    int last = where;
    while ( last < sb.length() && sb.charAt(last) == 'X' ) ++last;

    // substitute until done (FIXME: may be a forever in rare cases)
    for (;;) {
      // create and substitute
      for ( int i=where; i<last; ++i ) {
	sb.setCharAt(i, filenamechars.charAt( m_rng.nextInt( filenamechars.length() ) ) );
      }

      // check uniqueness
      if ( suffix.length() > 0 ) 
	result = sb.toString() + suffix;
      if ( ! m_tempfiles.contains(result) ) {
	m_tempfiles.add(result);
	break;
      }
    }

    return result;
  }

  /**
   * Detects valid results in the ADAG as opposed to an empty shell. 
   *
   * @return true, if the ADAG is an empty shell.
   */
  public boolean isEmpty()
  {
    Iterator i = this.m_dax.iterateJob();
    return ( ! i.hasNext() );
  }

  /**
   * This method is a possibly more memory efficient version of 
   * constructing a DAG.
   * @param stream is a generic stream to put textual results onto.
   */
  public void toString( Writer stream )
    throws IOException
  { this.m_dax.toString(stream); }

  /**
   * dumps the state of this object into human readable format. 
   */
  public String toString()
  { return this.m_dax.toString(); }

  /**
   * This method is a possibly more memory efficient version of 
   * constructing a DAX.
   * @param stream is a generic stream to put XML results onto.
   * @param indent is the initial indentation level.
   * @param namespace is an optional XML namespace.
   */
  public void toXML( Writer stream, String indent, String namespace )
    throws IOException
  { this.m_dax.toXML(stream,indent,namespace); }

  /**
   * This method is a possibly more memory efficient version of 
   * constructing a DAX.
   * @param stream is a generic stream to put XML results onto.
   * @param indent is the initial indentation level.
   */
  public void toXML( Writer stream, String indent )
    throws IOException
  { this.m_dax.toXML(stream,indent,null); }

  /**
   * Dumps the state of this object into machine readable XML. 
   * @param indent is the initial indentation level.
   * @param namespace is an optional XML namespace.
   */
  public String toXML( String indent, String namespace )
  { return this.m_dax.toXML(indent,namespace); }
}
